package javax.generic.swing;

import java.awt.Color;
import java.awt.Container;
import java.awt.Dialog;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JTextField;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;

import org.apache.log4j.Logger;

public class VisualEditor<T> extends JDialog {

	private static final long serialVersionUID = 8450047064601251038L;

	private static final Logger LOG = Logger.getLogger(VisualEditor.class);

	private final T object;
	private List<JComponent> components = new ArrayList<JComponent>();
	private List<FieldInfo> visualFields = new ArrayList<FieldInfo>();

	private JButton okButton;
	private Map<String, JButton> buttons = new HashMap<String, JButton>();
	private Map<String, JComponent> fields = new HashMap<String, JComponent>();

	public VisualEditor(final T object, Dialog owner) {
		super(owner, true);
		this.object = object;
		build();
		postConstruct();
		pack();
	}

	public VisualEditor(final T object) {
		super();
		this.object = object;
		build();
		postConstruct();
		pack();
	}

	private void build() {
		// setResizable(false);

		GridBagLayout layout = new GridBagLayout();
		setLayout(layout);

		visualFields = ClassUtils.extractVisualFields(object.getClass(), false);

		Collections.sort(visualFields, new Comparator<FieldInfo>() {

			@Override
			public int compare(FieldInfo o1, FieldInfo o2) {

				VisualField anotation1 = ClassUtils.getFieldAnotation(o1.getField());
				VisualField anotation2 = ClassUtils.getFieldAnotation(o2.getField());
				Integer i1 = anotation1.position();
				Integer i2 = anotation2.position();
				return i1.compareTo(i2);
			}

		});
		for (int i = 0; i < visualFields.size(); i++) {
			addVisualComponent(this, i, visualFields.get(i));
		}

		okButton = addButton(this, 0, visualFields.size(), "OK");

		okButton.addActionListener(new ActionListener() {

			@Override
			public void actionPerformed(ActionEvent arg0) {
				onOk(object);
			}
		});

		okButton.addKeyListener(new java.awt.event.KeyAdapter() {
			@Override
			public void keyPressed(java.awt.event.KeyEvent e) {
				if (java.awt.event.KeyEvent.VK_ESCAPE == e.getKeyCode()) {
					try {
						VisualEditor.this.finalize();
					} catch (Throwable e1) {
					}
				VisualEditor.this.dispose();
			}
			if (java.awt.event.KeyEvent.VK_ENTER == e.getKeyCode()) {
				onOk(object);
			}
		}
		});

		JButton cancelButton = addButton(this, 1, visualFields.size(), "Cancel");
		cancelButton.addActionListener(new ActionListener() {

			@Override
			public void actionPerformed(ActionEvent e) {
				onCancel();
			}
		});
	}

	public void postConstruct() {

	}

	/**
	 * Could be overridden.
	 * 
	 * @param fieldInfo
	 * @param newValue
	 */
	public void fieldHasChanged(FieldInfo fieldInfo, Object newValue, T object) {

	}

	public void onCancel() {
		dispose();
	}

	public void onOk(T object) {
		dispose();
	}

	public void addButtonListener(String name, ActionListener actionListener) {
		JButton jButton = buttons.get(name);
		if (jButton != null) {
			jButton.addActionListener(actionListener);
		}
	}

	public void refreshViewFromModel() {
		for (int i = 0; i < visualFields.size(); i++) {
			FieldInfo fieldInfo = visualFields.get(i);
			Object value = ClassUtils.getValue(fieldInfo, object);
			JComponent jComponent = fields.get(fieldInfo.getField().getName());
			if (jComponent == null) {
				continue;
			}
			VisualField fieldAnotation = ClassUtils.getFieldAnotation(fieldInfo.getField());
			if (fieldAnotation.type().equals(VisualFieldType.ENUM)) {
				JComboBox box = (JComboBox)jComponent;
				box.setSelectedItem(value);
			} else {
				JTextField field = (JTextField)jComponent;
				if (value != null) {
					field.setText(value.toString());
				}
			}

		}
		repaint();
	}

	public T getObject() {
		return object;
	}

	protected void showInfoMSg(String msg) {
		JOptionPane.showMessageDialog(this, msg, "Obaveštenje", JOptionPane.INFORMATION_MESSAGE);
	}

	protected void showErrorMSg(String msg) {
		JOptionPane.showMessageDialog(this, msg, "Greska", JOptionPane.ERROR_MESSAGE);
	}

	private void addVisualComponent(Container dialog, int y, final FieldInfo fieldInfo) {
		LOG.info("building field " + fieldInfo);
		VisualField visualField = ClassUtils.getFieldAnotation(fieldInfo.getField());
		if (visualField.isTransient()) {
			return;
		}
		GridBagConstraints labelConstraints = new GridBagConstraints();
		labelConstraints.gridx = 0;
		labelConstraints.gridy = y;
		labelConstraints.gridwidth = 1;
		labelConstraints.gridheight = 1;
		labelConstraints.anchor = GridBagConstraints.EAST;
		labelConstraints.insets = new Insets(5, 0, 0, 0);

		JLabel label = new JLabel(visualField.label());
		dialog.add(label, labelConstraints);

		GridBagConstraints textConstraints = new GridBagConstraints();
		textConstraints.gridx = 1;
		textConstraints.gridy = y;
		textConstraints.gridwidth = 3;
		textConstraints.gridheight = 1;
		textConstraints.anchor = GridBagConstraints.WEST;
		textConstraints.insets = new Insets(5, 0, 0, 0);

		Object value = ClassUtils.getValue(fieldInfo, object);
		if (visualField.type().equals(VisualFieldType.ENUM)) {
			textConstraints.ipadx = 70;
			Class<?> clazz;
			try {
				clazz = Class.forName(fieldInfo.getField().getType().getName());
				Method mainMethod = clazz.getMethod("values", new Class[] {});
				final JComboBox box = new JComboBox((Enum[])mainMethod.invoke(null, new Object[] {}));
				box.setEnabled(!fieldInfo.isComplex());

				box.setSelectedItem(value);
				box.addActionListener(new ActionListener() {

					@Override
					public void actionPerformed(ActionEvent arg0) {
						Enum<?> selectedItem = (Enum<?>)box.getSelectedItem();
						if (selectedItem == null) {
							setNewValue(fieldInfo, "");
						} else {
							setNewValue(fieldInfo, selectedItem.name());
						}

					}
				});

				box.addKeyListener(new FocusNext(box, fieldInfo) {

					@Override
					public String newValue() {
						Enum<?> selectedItem = (Enum<?>)box.getSelectedItem();
						if (selectedItem == null) {
							return "";
						} else {
							return selectedItem.name();
						}

					}
				});

				dialog.add(box, textConstraints);
				components.add(box);
				fields.put(fieldInfo.getField().getName(), box);
			} catch (Exception e) {
				e.printStackTrace();
			}

		} else {
			textConstraints.ipadx = 165;
			final JTextField textField = new JTextField(20);
			if (fieldInfo.isComplex()) {
				textField.setEditable(false);
			}
			if (value != null) {
				textField.setText(value.toString());
			}
			textField.addFocusListener(new FocusAdapter() {
				@Override
				public void focusGained(FocusEvent e) {
					textField.setSelectionStart(0);
					textField.setSelectionEnd(textField.getText().length());
				}
			});
			textField.addKeyListener(new FocusNext(textField, fieldInfo) {

				@Override
				public String newValue() {
					return textField.getText();
				}
			});

			textField.getDocument().addDocumentListener(new DocumentListener() {

				@Override
				public void insertUpdate(DocumentEvent e) {
					setNewValue(fieldInfo, textField.getText());
				}

				@Override
				public void removeUpdate(DocumentEvent e) {
					setNewValue(fieldInfo, textField.getText());
				}

				@Override
				public void changedUpdate(DocumentEvent e) {
					setNewValue(fieldInfo, textField.getText());
				}

			});

			components.add(textField);
			fields.put(fieldInfo.getField().getName(), textField);
			dialog.add(textField, textConstraints);
			if (fieldInfo.isComplex() && visualField.actionIfComplexMember()) {
				GridBagConstraints actionConstraints = new GridBagConstraints();
				actionConstraints.gridx = 4;
				actionConstraints.gridy = y;
				actionConstraints.gridwidth = 1;
				actionConstraints.gridheight = 1;
				actionConstraints.anchor = GridBagConstraints.WEST;
				actionConstraints.insets = new Insets(5, 0, 0, 0);
				JButton actionButton = new JButton("...");

				buttons.put(fieldInfo.getField().getName(), actionButton);
				dialog.add(actionButton, actionConstraints);
			}
		}
	}

	private JButton addButton(Container dialog, int x, int y, String label) {
		GridBagConstraints constraints = new GridBagConstraints();
		constraints.gridx = x;
		constraints.gridy = y;
		constraints.gridwidth = 1;
		constraints.gridheight = 1;
		constraints.anchor = GridBagConstraints.EAST;
		constraints.insets = new Insets(5, 0, 0, 0);

		JButton button = new JButton(label);
		dialog.add(button, constraints);
		return button;
	}

	/**
	 * sets new value from UI to model.
	 * 
	 * @param fieldInfo
	 * @param value
	 */
	private void setNewValue(final FieldInfo fieldInfo, final String value) {
		JComponent jComponent = fields.get(fieldInfo.getField().getName());
		jComponent.setBackground(Color.white);
		Object newValue = null;
		try {
			newValue = ClassUtils.setValue(object, fieldInfo, value);
			fieldHasChanged(fieldInfo, newValue, object);
		} catch (Exception e) {
			LOG.error("failed on field" + fieldInfo + " for value:" + newValue, e);
			jComponent.setBackground(Color.red);
			LOG.info(e);
		}
	}

	private abstract class FocusNext implements KeyListener {

		private JComponent textField;
		private FieldInfo fieldInfo;

		public FocusNext(JComponent textField, FieldInfo fieldInfo) {
			super();
			this.textField = textField;
			this.fieldInfo = fieldInfo;
		}

		@Override
		public void keyPressed(KeyEvent e) {}

		@Override
		public void keyReleased(KeyEvent e) {}

		@Override
		public void keyTyped(KeyEvent e) {
			setNewValue(fieldInfo, newValue());
			onKeyTyped(textField, e);
		}

		public abstract String newValue();
	}

	private void onKeyTyped(final JComponent textField, KeyEvent e) {
		if (e.getKeyChar() == KeyEvent.VK_ESCAPE) {
			try {
				finalize();
			} catch (Throwable e1) {}
			dispose();
		}
		if (e.getKeyChar() == KeyEvent.VK_BACK_SPACE) {
			int indexOf = components.indexOf(textField);
			if (indexOf > 0) {
				if (textField instanceof JTextField) {
					if (((JTextField)textField).getText().isEmpty()) {
						components.get(indexOf - 1).requestFocus();
					}
				} else {
					components.get(indexOf - 1).requestFocus();
				}

			}
		} else if (e.getKeyChar() == KeyEvent.VK_ENTER) {
			int indexOf = components.indexOf(textField);
			if (components.size() - 1 > indexOf) {
				components.get(indexOf + 1).requestFocus();
			} else {
				okButton.requestFocus();
			}
		}
	}
}
